package dkbta.processing;

import static dkbta.processing.Patients.print;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import dkbta.ontology.elements.Element;
import dkbta.ontology.elements.ElementDef;
import dkbta.ontology.elements.ElementType;
import dkbta.ontology.elements.abstractions.gradient.Gradient;
import dkbta.ontology.elements.abstractions.rate.Rate;
import dkbta.ontology.elements.abstractions.state.State;
import dkbta.ontology.elements.abstractions.trend.Trend;
import dkbta.ontology.elements.context.Context;
import dkbta.ontology.elements.event.Event;
import dkbta.ontology.elements.pattern.Pattern;
import dkbta.ontology.elements.primitive.Primitive;
import dkbta.ontology.elements.repeatingpattern.RepeatingPattern;
import dkbta.ontology.elements.subjectstat.SubjectStat;
import dkbta.processing.data.DataBatch;
import dkbta.processing.data.DataDispatcher;
import dkbta.processing.data.SendTo;
import dkbta.processing.tasks.LocalTask;
import dkbta.processing.tasks.RemoteTask;
import dkbta.processing.tasks.Task;

/**
 * A class representing a single patient in the system. It holds all of the instances
 * associated with it.
 * 
 * @author Uri Kanonov
 */
public class Patient implements Comparable<Patient>{
	/** This patient's id */
	private final int _patientId;

	/** The primitive instances associated with this patient */
	private final Map<String, List<Primitive>> _primitives;

	/** The event instances associated with this patient */
	private final Map<String, List<Event>> _events;

	/** The context instances associated with this patient */
	private final Map<String, List<Context>> _contexts;

	/** The state instances associated with this patient */
	private final Map<String, List<State>> _states;

	/** The gradient instances associated with this patient */
	private final Map<String, List<Gradient>> _gradients;

	/** The rate instances associated with this patient */
	private final Map<String, List<Rate>> _rates;

	/** The trend instances associated with this patient */
	private final Map<String, List<Trend>> _trends;

	/** The pattern instances associated with this patient */
	private final Map<String, List<Pattern>> _patterns;

	/** The repeating pattern instances associated with this patient */
	private final Map<String, List<RepeatingPattern>> _repeatingPatterns;

	/** The subject-statistical abstraction instances associated with this patient */
	private final Map<String, List<SubjectStat>> _subjectStats;

	/** The elements that have already been computed, i.e. all instances are ready */
	private final Set<ElementDef> _computedElements;

	/** The elements that still need to be computed by a remote unit */
	private final Map<ElementDef, Task> _remotelyComputedElements;

	/**
	 * The elements that need to be computed locally for this patient and their associated
	 * send-to information (who to send the instances upon computation finish)
	 */
	private final Map<ElementDef, SendTo> _locallyComputedElements;

	/** A reference to the work queue */
	private final WorkQueue _wq;

	/** A reference to the data dispatcher */
	private final DataDispatcher _dd;

	/** The patient's hash code, it is cached so it won't be computed each time */
	private final int _hashCode;

	/**
	 * Initializes a patient
	 * 
	 * @param patientId The patient's id
	 * @param wq A reference to the work queue
	 * @param dd A reference to the data dispatcher
	 * @param remotelyComputedElements Set of elements that are to be computed remotely
	 * @param locallyComputedElements Set of elements that are to be computed locally and
	 *        their associated send-to information (who to send the instances upon
	 *        computation finish)
	 */
	public Patient(int patientId, WorkQueue wq, DataDispatcher dd,
		Set<ElementDef> remotelyComputedElements,
		Map<ElementDef, SendTo> locallyComputedElements){
		_patientId = patientId;
		_dd = dd;
		_locallyComputedElements = Collections.unmodifiableMap(locallyComputedElements);
		_hashCode = Integer.valueOf(patientId).hashCode();
		_wq = wq;
		_primitives = new HashMap<String, List<Primitive>>();
		_events = new HashMap<String, List<Event>>();
		_contexts = new HashMap<String, List<Context>>();
		_states = new HashMap<String, List<State>>();
		_gradients = new HashMap<String, List<Gradient>>();
		_rates = new HashMap<String, List<Rate>>();
		_trends = new HashMap<String, List<Trend>>();
		_patterns = new HashMap<String, List<Pattern>>();
		_repeatingPatterns = new HashMap<String, List<RepeatingPattern>>();
		_subjectStats = new HashMap<String, List<SubjectStat>>();
		_computedElements = new HashSet<ElementDef>();
		_remotelyComputedElements = new HashMap<ElementDef, Task>();
		for (ElementDef ed : remotelyComputedElements){
			_remotelyComputedElements.put(ed, null);
		}
	}

	/**
	 * Returns this patient's id
	 * 
	 * @return This patient's id
	 */
	public int getPatientId(){
		return _patientId;
	}

	/**
	 * Compares patients based on their id
	 * 
	 * @param o The patient to be compared.
	 * @return A negative integer, zero, or a positive integer as this patient's id is
	 *         less than, equal to, or greater than the specified patient's id.
	 */
	@Override
	public int compareTo(Patient o){
		return _patientId - o._patientId;
	}

	/**
	 * Another patient is equal to other patients with the same Id.
	 * 
	 * @param obj The object to be compared to
	 * @return Whether this patient is equal to the given object
	 */
	@Override
	public boolean equals(Object obj){
		return (obj instanceof Patient) && ((Patient)obj).getPatientId() == _patientId;
	}

	@Override
	public int hashCode(){
		return _hashCode;
	}

	public Set<ElementDef> getRemoteElements(){
		return Collections.unmodifiableSet(_remotelyComputedElements.keySet());
	}

	/**
	 * Retrieving all the elements this patient needs for its computation.
	 * 
	 * @return The element definitions of the primitives and events for the patient
	 */
	public SortedSet<ElementDef> getRequiredPrimsAndEvents(){
		SortedSet<ElementDef> requiredPrimsAndEvents = new TreeSet<ElementDef>(
				new Comparator<ElementDef>(){
					@Override
					public int compare(ElementDef o1, ElementDef o2){
						int compareTo = o1.getName().compareToIgnoreCase(o2.getName());
						if (compareTo == 0){
							compareTo = o1.getElementType()
									.compareTo(o2.getElementType());
						}
						return compareTo;
					}
				});
		for (ElementDef localElement : _locallyComputedElements.keySet()){
			localElement.addRequiredPrimsAndEvents(requiredPrimsAndEvents);
		}
		return requiredPrimsAndEvents;
	}

	@Override
	public String toString(){
		StringBuilder sb = new StringBuilder("Patient: ");
		sb.append(_patientId);
		sb.append("\r\nComputed:\r\n");
		for (ElementDef ed : _computedElements){
			sb.append("\t").append(ed).append("\r\n");
		}
		sb.append("Local Elements:\r\n");
		for (Map.Entry<ElementDef, SendTo> entry : _locallyComputedElements.entrySet()){
			sb.append("\t").append(entry.getKey()).append("\r\n");
		}
		sb.append("Remote Elements:\r\n");
		for (ElementDef ed : _remotelyComputedElements.keySet()){
			sb.append("\t").append(ed).append("\r\n");
		}
		sb.append("\r\n");
		return sb.toString();
	}

	/**
	 * Notes that the given single-patient element has been computed. <br>
	 * If the element was to be remotely computed and it had a remote task pending, then
	 * that task is deemed completed and its parent tasks are notified of the completion.
	 * <br>
	 * If the element was was to be locally computed and it had associated send-to
	 * information then its elements are given to the <tt>Data Dispatcher</tt> for
	 * sending to the db/units
	 * 
	 * @param element The element that has been computed
	 */
	public synchronized void addComputedElement(ElementDef ed){
		// Indicating that the element has been computed
		if (!_computedElements.add(ed)){
			return;
		}

		// If the element was to be remotely computed and had some tasks
		// waiting for it to be computed, we notify them that the element is ready
		Task remoteComputationTask = _remotelyComputedElements.remove(ed);
		if (remoteComputationTask != null){
			remoteComputationTask.notifyParentTasksAboutCompletion(_wq);
			return;
		}

		// Checking if the computed data was to be sent to another unit
		SendTo sendTo = _locallyComputedElements.get(ed);
		if (sendTo != null){
			_dd.addDataBatch(new DataBatch(ed, sendTo, getElements(ed.getElementType(),
				ed.getName()), _patientId));
		}
	}

	/**
	 * Checks whether the given element has already been computed, if so this method
	 * returns true. Otherwise the following occurs:
	 * <ol>
	 * <li>If the element was to be remotely computed, a remote task is created and
	 * associated with the element (it is not added to the work queue). Otherwise (it is
	 * to be locally computed) a local task is created and added to the work queue</li>
	 * <li>The created task is added as a child task (a prerequisite) to the given parent
	 * task</li>
	 * <li>The created task saves a reference to the parent task for notification upon
	 * completion</li>
	 * </ol>
	 * 
	 * @param element The element to be checked if already computed or not.
	 * @param wq The work queue
	 * @param parentTask The parent task that requires the given element to be computed
	 *        (it is a prerequisite for its completion)
	 * @return If the element has already been computed, false otherwise
	 */
	public synchronized boolean hasBeenComputed(ElementDef element, WorkQueue wq,
		Task parentTask){
		if (_computedElements.contains(element)){
			// The element's instances have all been computed and can safely be
			// queried from the patient
			return true;
		}

		Task task;
		if (_remotelyComputedElements.containsKey(element)){
			// The element is to be computed remotely and still hasn't been computed
			// (as indicated by the fact it is not in "computedElements").
			// Creating a remote computation task and indicating it should not be added
			// to the work queue which is meant for local computation tasks
			task = wq.newTask(new RemoteTask(element, this), false);
			_remotelyComputedElements.put(element, task);
		}else{
			// The element is to be computed locally and still hasn't been computed.
			// Creating a task to compute the instances of the prerequisite
			// element and adding it to the work queue
			task = wq.newTask(new LocalTask(element, false, this), true);
		}

		// Telling the parent task it has a prerequisite task
		parentTask.addChildTask();
		// Telling the prerequisite task who to inform once it is completed
		task.addParentTask(parentTask);
		return false;
	}

	/**
	 * Checks that each of the prerequisites of the given elements is either locally
	 * computed or remotely received
	 * 
	 * @param elements The elements
	 * @throws PlanException If a prerequisite is neither local nor remote
	 */
	public void assertComputationOfPrerequisites(Collection<? extends ElementDef> elements)
			throws PlanException{
		for (ElementDef element : elements){
			Collection<ElementDef> prerequisites = element.getPrerequisites();
			for (ElementDef prerequisite : prerequisites){
				if (!(_locallyComputedElements.containsKey(prerequisite) || _remotelyComputedElements
						.containsKey(prerequisite))){
					throw new PlanException("The element " + prerequisite
							+ " required by " + element + " is not computed by patient "
							+ _patientId);
				}
			}
		}
	}

	/**
	 * Adds a primitive to the patient
	 * 
	 * @param primitive The primitive
	 */
	public void addPrimitive(Primitive primitive){
		add(primitive, _primitives);
	}

	/**
	 * Adds an event to the patient
	 * 
	 * @param event The event
	 */
	public void addEvent(Event event){
		add(event, _events);
	}

	/**
	 * Adds a context to the patient
	 * 
	 * @param primitive The context
	 */
	public void addContext(Context context){
		add(context, _contexts);
	}

	/**
	 * Adds a state to the patient
	 * 
	 * @param state The state
	 */
	public void addState(State state){
		add(state, _states);
	}

	/**
	 * Adds a gradient to the patient
	 * 
	 * @param gradient The gradient
	 */
	public void addGradient(Gradient gradient){
		add(gradient, _gradients);
	}

	/**
	 * Adds a rate to the patient
	 * 
	 * @param rate The rate
	 */
	public void addRate(Rate rate){
		add(rate, _rates);
	}

	/**
	 * Adds a trend to the patient
	 * 
	 * @param trend The trend
	 */
	public void addTrend(Trend trend){
		add(trend, _trends);
	}

	/**
	 * Adds a pattern to the patient
	 * 
	 * @param pattern The pattern
	 */
	public void addPattern(Pattern pattern){
		add(pattern, _patterns);
	}

	/**
	 * Adds a repeating pattern to the patient
	 * 
	 * @param repeatingPattern The repeating pattern
	 */
	public void addRepeatingPattern(RepeatingPattern repeatingPattern){
		add(repeatingPattern, _repeatingPatterns);
	}

	/**
	 * Adds a subject-statistical abstraction to the patient
	 * 
	 * @param subjectStat The subject-statistical abstraction
	 */
	public void addSubjectStat(SubjectStat subjectStat){
		add(subjectStat, _subjectStats);
	}

	/**
	 * Adds an element to the patient
	 * 
	 * @param element The element
	 */
	public void add(Element element){
		ElementType eType = element.getElementType();
		switch (eType){
			case PRIMITIVE:
				addPrimitive((Primitive)element);
				break;
			case EVENT:
				addEvent((Event)element);
				break;
			case CONTEXT:
				addContext((Context)element);
				break;
			case STATE:
				addState((State)element);
				break;
			case GRADIENT:
				addGradient((Gradient)element);
				break;
			case RATE:
				addRate((Rate)element);
				break;
			case TREND:
				addTrend((Trend)element);
				break;
			case PATTERN:
				addPattern((Pattern)element);
				break;
			case REPEATINGPATTERN:
				addRepeatingPattern((RepeatingPattern)element);
				break;
			case SUBJECTSTAT:
				addSubjectStat((SubjectStat)element);
				break;
			default:
				throw new UnsupportedOperationException("Adder not implemented for "
						+ eType);
		}
	}

	/**
	 * Retrieves the instances of the primitive from the patient
	 * 
	 * @param name The primitive's name
	 * @return The list of instances
	 */
	public List<Primitive> getPrimitives(String name){
		if (name == null){
			throw new NullPointerException("Name cannot be null");
		}
		return get(name, _primitives);
	}

	/**
	 * Retrieves the instances of the event from the patient
	 * 
	 * @param name The event's name
	 * @return The list of instances
	 */
	public List<Event> getEvents(String name){
		if (name == null){
			throw new NullPointerException("Name cannot be null");
		}
		return get(name, _events);
	}

	/**
	 * Retrieves the instances of the context from the patient
	 * 
	 * @param name The context's name
	 * @return The list of instances
	 */
	public List<Context> getContexts(String name){
		if (name == null){
			throw new NullPointerException("Name cannot be null");
		}
		return get(name, _contexts);
	}

	/**
	 * Retrieves the instances of the state from the patient
	 * 
	 * @param name The state's name
	 * @return The list of instances
	 */
	public List<State> getStates(String name){
		if (name == null){
			throw new NullPointerException("Name cannot be null");
		}
		return get(name, _states);
	}

	/**
	 * Retrieves the instances of the gradient from the patient
	 * 
	 * @param name The gradient's name
	 * @return The list of instances
	 */
	public List<Gradient> getGradients(String name){
		if (name == null){
			throw new NullPointerException("Name cannot be null");
		}
		return get(name, _gradients);
	}

	/**
	 * Retrieves the instances of the rates from the patient
	 * 
	 * @param name The rate's name
	 * @return The list of instances
	 */
	public List<Rate> getRates(String name){
		if (name == null){
			throw new NullPointerException("Name cannot be null");
		}
		return get(name, _rates);
	}

	/**
	 * Retrieves the instances of the trends from the patient
	 * 
	 * @param name The trend's name
	 * @return The list of instances
	 */
	public List<Trend> getTrends(String name){
		if (name == null){
			throw new NullPointerException("Name cannot be null");
		}
		return get(name, _trends);
	}

	/**
	 * Retrieves the instances of the pattern from the patient
	 * 
	 * @param name The pattern's name
	 * @return The list of instances
	 */
	public List<Pattern> getPatterns(String name){
		if (name == null){
			throw new NullPointerException("Name cannot be null");
		}
		return get(name, _patterns);
	}

	/**
	 * Retrieves the instances of the repeating pattern from the patient
	 * 
	 * @param name The repeating pattern's name
	 * @return The list of instances
	 */
	public List<RepeatingPattern> getRepeatingPatterns(String name){
		if (name == null){
			throw new NullPointerException("Name cannot be null");
		}
		return get(name, _repeatingPatterns);
	}

	/**
	 * Retrieves the instances of the subject-statistical abstraction from the patient
	 * 
	 * @param name The subject-statistical abstraction's name
	 * @return The list of instances
	 */
	public List<SubjectStat> getSubjectStats(String name){
		if (name == null){
			throw new NullPointerException("Name cannot be null");
		}
		return get(name, _subjectStats);
	}

	/**
	 * Retrieves the instances of an element from the patient according to the name and
	 * type
	 * 
	 * @param elementType The element's type
	 * @param elementName The element's name
	 * @return The list of instances or null if the name is null, eType is null or the
	 *         element has no instances
	 */
	public List<? extends Element> getElements(ElementType elementType, String elementName){
		if (elementType == null || elementName == null){
			throw new NullPointerException("Element type and element name cannot be null");
		}else{
			switch (elementType){
				case PRIMITIVE:
					return getPrimitives(elementName);
				case EVENT:
					return getEvents(elementName);
				case CONTEXT:
					return getContexts(elementName);
				case STATE:
					return getStates(elementName);
				case GRADIENT:
					return getGradients(elementName);
				case RATE:
					return getRates(elementName);
				case TREND:
					return getTrends(elementName);
				case PATTERN:
					return getPatterns(elementName);
				case REPEATINGPATTERN:
					return getRepeatingPatterns(elementName);
				case SUBJECTSTAT:
					return getSubjectStats(elementName);
				default:
					throw new UnsupportedOperationException("Getter not implemented for "
							+ elementType);
			}
		}
	}

	/**
	 * Returns the list of instances for the specified element
	 * 
	 * @param name The element name
	 * @param elementsByName All of the instance's of the element's type
	 */
	private <T extends Element> List<T> get(String name,
		Map<String, List<T>> elementsByName){
		List<T> elements = elementsByName.get(name);

		if (elements == null){
			elements = new ArrayList<T>();
			elementsByName.put(name, elements);
		}
		return elements;
	}

	/**
	 * Adds an element to the patient
	 * 
	 * @param element The element
	 */
	private <T extends Element> void add(T element, Map<String, List<T>> elementsByName){
		String name = element.getName();
		List<T> elements = elementsByName.get(name);

		if (elements == null){
			elements = new ArrayList<T>();
			elementsByName.put(name, elements);
		}
		elements.add(element);
	}

	/**
	 * Prints the instances stored for this patient
	 */
	public void printInstances(boolean summarizePrimitives, boolean summarizeEvents,
		boolean summarizeContexts, boolean summarizeStates, boolean summarizeGradients,
		boolean summarizeRates, boolean summarizeTrends, boolean summarizePatterns,
		boolean summarizeRepeatingPatterns, boolean summarizeSubjectStats,
		Set<String> detailedElements){
		print(_primitives, summarizePrimitives, "Primitives", detailedElements);
		print(_events, summarizeEvents, "Events", detailedElements);
		print(_contexts, summarizeContexts, "Contexts", detailedElements);
		print(_states, summarizeStates, "States", detailedElements);
		print(_gradients, summarizeGradients, "Gradients", detailedElements);
		print(_rates, summarizeRates, "Rates", detailedElements);
		print(_trends, summarizeTrends, "Trends", detailedElements);
		print(_patterns, summarizePatterns, "Patterns", detailedElements);
		print(_repeatingPatterns, summarizeRepeatingPatterns, "Repeating Patterns",
			detailedElements);
		print(_subjectStats, summarizeSubjectStats, "Subject-Statistical Abstractions",
			detailedElements);
	}

}
